package com.breeze.blockchain.mybatis.entity;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
import com.breeze.blockchain.auth.AuthContextHolder;
import com.breeze.components.core.enums.EnumInterface;
import com.breeze.components.core.enums.OperationStatusEnum;
import com.breeze.components.core.exception.ApiException;
import com.breeze.components.core.util.json.Jackson;
import com.breeze.components.core.util.web.ServletHolderUtil;
import com.breeze.components.mybatis.base.BaseEntity;
import com.breeze.components.web.enums.RequestMethodEnum;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mybatisflex.annotation.Table;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;

/**
 * 操作日志记录 实体类。
 *
 * @author breeze
 * @since 2024-05-11 23:21:51
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Schema(title = "操作日志记录")
@Table("sys_operation_log")
@Slf4j
public class OperationLogDo extends BaseEntity implements Serializable {

  @Serial private static final long serialVersionUID = 1L;

  public static final int MAX_DATA_LENGTH = 512;

  /** 业务类型（0其它 1新增 2修改 3删除） */
  @Schema(title = "业务类型（0其它 1新增 2修改 3删除）")
  private Integer businessType;

  /** 请求方式 */
  @Schema(title = "请求方式")
  private Integer requestMethod;

  /** 请求模块 */
  @Schema(title = "请求模块")
  private String requestModule;

  /** 请求URL */
  @Schema(title = "请求URL")
  private String requestUrl;

  /** 调用方法 */
  @Schema(title = "调用方法")
  private String calledMethod;

  /** 操作类别（0其它 1后台用户 2手机端用户） */
  @Schema(title = "操作类别（0其它 1后台用户 2手机端用户）")
  private Integer operatorType;

  /** 用户ID */
  @Schema(title = "用户ID")
  private Long userId;

  /** 操作人员 */
  @Schema(title = "操作人员")
  private String username;

  /** 操作人员ip */
  @Schema(title = "操作人员ip")
  private String operatorIp;

  /** 操作地点 */
  @Schema(title = "操作地点")
  private String operatorLocation;

  /** 部门ID */
  @Schema(title = "部门ID")
  private Long deptId;

  /** 部门名称 */
  @Schema(title = "部门名称")
  private String deptName;

  /** 请求参数 */
  @Schema(title = "请求参数")
  private String operationParam;

  /** 返回参数 */
  @Schema(title = "返回参数")
  private String operationResult;

  /** 操作状态（1正常 0异常） */
  @Schema(title = "操作状态（1正常 0异常）")
  private Integer status;

  /** 错误消息 */
  @Schema(title = "错误消息")
  private String errorStack;

  /** 操作时间 */
  @Schema(title = "操作时间")
  @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
  private LocalDateTime operationTime;

  public void fillAppOperatorInfo() {
    // 获取当前的用户
    var ip = JakartaServletUtil.getClientIP(ServletHolderUtil.getRequest());
    setOperatorIp(ip);
    UserDo loginUser = null;
    try {
      loginUser = AuthContextHolder.getAppLoginUser();
    } catch (ApiException ignore) {
      log.info("获取当前app用户失败");
    }
    if (loginUser != null) {
      this.setUserId(loginUser.getId());
      this.setUsername(loginUser.getNickname());
    }
  }

  public void fillOperatorInfo() {
    // 获取当前的用户
    var ip = JakartaServletUtil.getClientIP(ServletHolderUtil.getRequest());
    setOperatorIp(ip);
    AdminDo loginUser = null;
    try {
      loginUser = AuthContextHolder.getAdminLoginUser();
    } catch (ApiException ignore) {
      log.info("获取当前admin用户失败");
    }
    if (loginUser != null) {
      this.setUserId(loginUser.getId());
      this.setUsername(loginUser.getUsername());
    }

    this.setDeptName(TraceContext.traceId());
    this.setOperationTime(LocalDateTime.now());
  }

  public void fillRequestInfo(final JoinPoint joinPoint, Object jsonResult) {
    HttpServletRequest request = ServletHolderUtil.getRequest();
    this.setRequestUrl(request.getRequestURI());
    // 设置方法名称
    var className = joinPoint.getTarget().getClass().getName();
    var methodName = joinPoint.getSignature().getName();
    var methodFormat = StrUtil.format("{}.{}()", className, methodName);
    this.setCalledMethod(methodFormat);
    // 设置请求方式
    var requestMethodEnum = EnumUtil.fromString(RequestMethodEnum.class, request.getMethod());
    this.setRequestMethod(
        requestMethodEnum != null
            ? requestMethodEnum.getCode()
            : RequestMethodEnum.UNKNOWN.getCode());
    // 是否需要保存request，参数和值
    // 获取参数的信息，传入到数据库中。
    recordRequestData(joinPoint);
    // 是否需要保存response，参数和值
    if (jsonResult != null) {
      this.setOperationResult(StrUtil.sub(JSONUtil.toJsonStr(jsonResult), 0, MAX_DATA_LENGTH));
    }
  }

  public void fillAccessLogInfo(JoinPoint log) {
    // 设置action动作
    var controllerAnnotation = log.getTarget().getClass().getAnnotation(Tag.class);
    var method = ((MethodSignature) log.getSignature()).getMethod().getAnnotation(Operation.class);
    // 设置标题
    this.setRequestModule(
        String.format(
            "%s:%s",
            Optional.ofNullable(controllerAnnotation).map(Tag::name).orElse(null),
            Optional.ofNullable(method).map(Operation::summary).orElse(null)));
  }

  public void fillStatus(Exception e) {
    if (e != null) {
      this.setStatus(OperationStatusEnum.FAIL.getCode());
      this.setErrorStack(StrUtil.sub(e.getMessage(), 0, MAX_DATA_LENGTH));
    } else {
      this.setStatus(OperationStatusEnum.SUCCESS.getCode());
    }
  }

  /**
   * 获取请求的参数，放到log中
   *
   * @param joinPoint 方法切面
   */
  private void recordRequestData(JoinPoint joinPoint) {
    var requestMethodEnum =
        EnumInterface.fromCode(RequestMethodEnum.class, this.getRequestMethod());

    if (requestMethodEnum == RequestMethodEnum.GET || requestMethodEnum == RequestMethodEnum.POST) {
      var params = argsArrayToString(joinPoint.getArgs());
      this.setOperationParam(StrUtil.sub(params, 0, MAX_DATA_LENGTH));
    } else {
      var paramsMap =
          (Map<?, ?>)
              ServletHolderUtil.getRequest()
                  .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
      this.setOperationParam(StrUtil.sub(paramsMap.toString(), 0, MAX_DATA_LENGTH));
    }
  }

  /** 参数拼装 */
  private String argsArrayToString(Object[] paramsArray) {
    var params = new StringBuilder();
    if (paramsArray != null) {
      for (var o : paramsArray) {
        if (o != null && !isCanNotBeParseToJson(o)) {
          try {
            var jsonObj = JSONUtil.parseObj(o);
            params.append(jsonObj).append(",");
          } catch (JSONException e) {
            try {
              // 非对象类型 无法parse, 例如 String Long等
              params.append(Jackson.to(o)).append(",");
            } catch (Exception ex) {
              log.info("参数拼接错误", ex);
            }
          }
        }
      }
    }
    return params.toString().trim();
  }

  /**
   * 判断是否需要过滤的对象。
   *
   * @param o 对象信息。
   * @return 如果是需要过滤的对象，则返回true；否则返回false。
   */
  @SuppressWarnings("rawtypes")
  public boolean isCanNotBeParseToJson(final Object o) {
    var clazz = o.getClass();
    if (clazz.isArray()) {
      return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
    } else if (Collection.class.isAssignableFrom(clazz)) {
      var collection = (Collection) o;
      for (Object value : collection) {
        return value instanceof MultipartFile;
      }
    } else if (Map.class.isAssignableFrom(clazz)) {
      var map = (Map) o;
      for (Object value : map.entrySet()) {
        var entry = (Map.Entry) value;
        return entry.getValue() instanceof MultipartFile;
      }
    }
    return o instanceof MultipartFile
        || o instanceof HttpServletRequest
        || o instanceof HttpServletResponse
        || o instanceof BindingResult;
  }
}
